import type { ClientRequestArgs, IncomingMessage, OutgoingHttpHeaders } from 'http'
import { request as httpRequest } from 'http'
import { request as httpsRequest } from 'https'
import { basename, extname } from 'path'
import { createReadStream, statSync } from 'fs'

export type Response = IncomingMessage
export type HeadersInit = OutgoingHttpHeaders

interface RequestInit {
  // 是否获取headers
  isHeaders?: boolean
  headers?: HeadersInit
  method?: string
  timeout?: number
  data?: any
  type?: 'TEXT' | 'JSON' | 'BUFFER'
  encoding?: BufferEncoding
  args?: ClientRequestArgs
}
export interface RequestOpt extends RequestInit {
  // 是否stringify参数（非GET请求使用）
  isStringify?: boolean
}

export interface RequestUploadOpt extends RequestInit {
  filePath: string
  fileName: string
  onUploadProgress?: (status: 'open' | 'ing' | 'end', size?: number, fullSize?: number) => void
}

export interface RequestDownloadOpt extends RequestInit {
  // 是否stringify参数（非GET请求使用）
  isStringify?: boolean
  onDown?: (chunk?: Buffer, allLength?: number) => void
}

/**
 * 对象转参数
 * @param data
 */
export function queryParams(data: any): string {
  const _result = []

  if (typeof data !== 'object') return ''

  Object.entries(data).forEach(item => {
    const [key, value] = item

    if (value) return

    if (Array.isArray(value)) {
      value.forEach(_value => {
        _result.push(encodeURIComponent(key) + '[]=' + encodeURIComponent(_value))
      })
    } else {
      _result.push(
        encodeURIComponent(key) + '=' + encodeURIComponent(value as string | number | boolean)
      )
    }
  })

  return _result.length ? _result.join('&') : ''
}

function dataToFormData(boundary: string, key: string, value: string) {
  return `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`
}

/**
 * 描述   请求方法
 * @date 2022-01-21
 * @param {any} url:string
 * @param {any} sendData:ClientRequestArgs={}
 * @param {any} ing:(response:IncomingMessage
 * @returns {any}
 */
const requestInit = (
  url: string,
  sendData: ClientRequestArgs = {},
  ing: (response: IncomingMessage) => void
) => (url.startsWith('http://') ? httpRequest : httpsRequest)(url, sendData, ing)

/**
 * 上传
 * @param url
 * @param sendData
 * @param params
 */
export function upload(url: string, params: RequestUploadOpt) {
  return new Promise((resolve, reject) => {
    params.method = params.method || 'GET'
    params.args = params.args || { method: params.method }
    if (!params.args.method) params.args.method = params.method
    const boundary = '--' + Math.random().toString(16)
    const headers = Object.assign(
      {
        'content-type': 'multipart/from-data; boundary=' + boundary
      },
      params.headers
    )
    if (!params.fileName) params.fileName = basename(params.filePath, extname(params.filePath))
    const chunks: Buffer[] = []
    let size = 0
    function ing(response: IncomingMessage) {
      response.on('data', chunk => {
        chunks.push(chunk)
        size += chunk.length
      })
      response.on('end', () => {
        const data = Buffer.concat(chunks, size)
        resolve(data)
      })
    }
    const request = requestInit(url, params.args, ing)
    for (const header in headers) request.setHeader(header, headers[header] as string)
    if (params.data) {
      for (const i in params.data) {
        request.write(dataToFormData(boundary, i, params.data[i]))
      }
    }
    request.write(
      `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${params.fileName}"\r\n\r\n`
    )
    request.on('destroyed', () => {
      reject(new Error('destroy'))
    })
    request.on('error', err => {
      reject(err)
    })
    const fileSize = statSync(params.filePath).size
    const readStream = createReadStream(params.fileName, {
      highWaterMark: 15 * 1024,
      autoClose: true,
      start: 0,
      end: fileSize
    })
    readStream.on('open', () => {
      if (params.onUploadProgress) params.onUploadProgress('open')
    })
    readStream.on('data', () => {
      if (params.onUploadProgress) params.onUploadProgress('ing', readStream.bytesRead, fileSize)
    })
    readStream.on('end', () => {
      if (params.onUploadProgress) params.onUploadProgress('end')
      request.end('\r\n--' + boundary + '--\r\n')
    })
    readStream.pipe(request as unknown as NodeJS.WritableStream, { end: false })
  })
}

/**
 * 下载
 * @param url
 * @param sendData
 * @param params
 */
export function download(url: string, params: RequestDownloadOpt = {}) {
  return new Promise((resolve, reject) => {
    params.method = params.method || 'GET'
    params.args = params.args || { method: params.method }
    params.type = 'BUFFER'
    if (!params.args.method) params.args.method = params.method
    const headers = Object.assign({}, params.headers)
    const chunks: Buffer[] = []
    let size = 0
    function ing(response: IncomingMessage) {
      const allLength = Number(response.headers['content-length'] || 0)
      response.on('data', chunk => {
        if (params.onDown) {
          params.onDown(chunk, allLength)
          return
        }
        chunks.push(chunk)
        size += chunk.length
      })
      response.on('end', () => {
        if (response.statusCode && response.statusCode >= 400) {
          reject(new Error(response.statusCode + ''))
          return
        }
        let result: unknown
        if (params.onDown) {
          result = {
            msg: 'downloaded',
            allLength
          }
        } else result = Buffer.concat(chunks, size)
        if (params.isHeaders) resolve({ data: result, headers: response.headers })
        else resolve(result)
      })
    }
    const request = requestInit(url, params.args, ing)
    request.on('destroyed', () => reject(new Error('destroy')))
    request.on('error', err => reject(err))
    for (const header in headers) request.setHeader(header, headers[header] as string)
    if (params.data && params.method !== 'GET') {
      if (typeof params.data !== 'string') {
        const data = params.isStringify ? queryParams(params.data) : JSON.stringify(params.data)
        request.write(data)
      } else request.write(params.data)
    }
    request.end()
  })
}

/**
 * 请求
 * @param url
 * @param params
 */
export default function request<T>(url: string, params: RequestOpt = {}): Promise<T> {
  params = {
    ...params,
    method: params.method || 'GET',
    args: params.args || { method: params.method },
    type: params.type || 'JSON',
    timeout: params.timeout || 1000 * 60
  }

  params.args.method = params.args.method || params.method
  if (params.data && params.method === 'GET') url += `?${queryParams(params.data)}`
  const headers = params.headers || { 'content-type': 'application/json;charset=utf-8' }
  const chunks: Buffer[] = []
  let size = 0

  return new Promise((resolve, reject) => {
    // 响应监听
    function ing(response: IncomingMessage) {
      response.on('data', chunk => {
        chunks.push(chunk)
        size += chunk.length
      })
      response.on('end', () => {
        const data = Buffer.concat(chunks, size)

        if (response.statusCode && response.statusCode >= 400) {
          reject(new Error(data.toString()))
          return
        }
        let result: unknown
        switch (params.type) {
          case 'BUFFER':
            result = data
            break
          case 'JSON':
            try {
              result = JSON.parse(data.toString())
            } catch (e) {
              result = data.toString(params.encoding || 'utf8')
            }
            break
          case 'TEXT':
            result = data.toString(params.encoding || 'utf8')
            break
        }

        if (params.isHeaders) resolve({ data: result, headers: response.headers } as unknown as T)
        else resolve(result as unknown as T)
      })
    }

    const request = requestInit(url, params.args, ing)

    for (const header in headers) request.setHeader(header, headers[header] as string)

    // 非get请求
    if (params.data && params.method !== 'GET') {
      if (typeof params.data !== 'string') {
        const data = params.isStringify ? queryParams(params.data) : JSON.stringify(params.data)
        request.write(data)
      } else request.write(params.data)
    }

    request.on('destroyed', () => reject(new Error('destroy')))
    request.on('error', err => reject(err))

    request.end()
  })
}
